package com.btr.uimeter.probes.mouseclick;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.AbstractButton;
import javax.swing.SwingUtilities;

import com.btr.uimeter.UIMeterEvent;
import com.btr.uimeter.probes.UIComponetDataExtractor;
import com.btr.uimeter.probes.UIMeterComponentFilter;
import com.btr.uimeter.probes.UIMeterProbe;

/*****************************************************************************
 * A probe that will collect all mouse clicks on Components.
 * With this you can track what buttons are pressed or menus are selected.
 * This can help you to understand what features of a program are used most.
 * 
 * To find badly implemented blocking actions in the UI you can record
 * execution times for the triggered actions.
 * 
 * If you are not interested in all components you can install a 
 * UIMeterComponentFilter to reduce the amount of gathered data.
 * 
 * To configure what data is collected from the components you can 
 * provide a UIComponetDataExtractor.
 *   
 * @author Bernd Rosstauscher (java@rosstauscher.de) Copyright 2010
 ****************************************************************************/

public class UIMouseClickProbe extends UIMeterProbe implements AWTEventListener {
	
	private static final int NANOS_PER_SECOND = (1000*1000);

	// Event ID for all Mouse Click probing events 
	private static final String PROBE_EVENT_TYPE = "MCLK";
	
	private UIComponetDataExtractor dataExtractor;
	private List<UIMeterComponentFilter> filterList;
	private boolean recordExecutionTime;
	
	/*************************************************************************
	 * Constructor
	 ************************************************************************/
	
	public UIMouseClickProbe() {
		super();
		filterList = new LinkedList<UIMeterComponentFilter>();
		recordExecutionTime = false;
		dataExtractor = new DefaultDataExtractor();
	}
	
	/*************************************************************************
	 * start
	 * @see com.btr.uimeter.probes.UIMeterProbe#start()
	 ************************************************************************/
	
	public void start() {
		Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_EVENT_MASK);	
	}
	
	/*************************************************************************
	 * stop
	 * @see com.btr.uimeter.probes.UIMeterProbe#stop()
	 ************************************************************************/
	
	public void stop() {
		Toolkit.getDefaultToolkit().removeAWTEventListener(this);	
	}

	/*************************************************************************
	 * eventDispatched
	 * @see java.awt.event.AWTEventListener#eventDispatched(java.awt.AWTEvent)
	 ************************************************************************/
	
	public void eventDispatched(AWTEvent event) {
		if (isMouseClickOnComponent(event)) {
			Component src = (Component) event.getSource();
			if (isAcceptedByComponentFilter(src)) {
				Map<String, Object> eventData = extractData(src);
				if (isRecordExecutionTime()) {
					recordExecutionTimeAndGenerateEvent(eventData);
				} else {
					generateEvent(eventData);
				}
			}
		}
	}

	/*************************************************************************
	 * Checks if the given event is a mouse click on a component.
	 * @param event to analyze.
	 * @return true if it is a mouse click, else false.
	 ************************************************************************/
	
	private boolean isMouseClickOnComponent(AWTEvent event) {
		return event.getID() == MouseEvent.MOUSE_RELEASED 
					&& event.getSource() instanceof Component;
	}

	/*************************************************************************
	 * @param eventData
	 ************************************************************************/
	
	private void recordExecutionTimeAndGenerateEvent(final Map<String, Object> eventData) {
		final long start = System.nanoTime();
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				long currentTime = System.nanoTime();
				long executionTimeInNanos = currentTime-start;
				long executionTimeInSeconds = executionTimeInNanos / NANOS_PER_SECOND;
				eventData.put("ui_time", executionTimeInSeconds);
				generateEvent(eventData);
			}
		});
	}

	/*************************************************************************
	 * Generates an UIMeterEvent and propagates it to all registered handlers.
	 * @param data to attache to the event. 
	 ************************************************************************/
	
	private void generateEvent(Map<String, Object> data) {
		UIMeterEvent evt = new UIMeterEvent(PROBE_EVENT_TYPE, data);
		fireProbeEvent(evt);
	}

	/*************************************************************************
	 * Extracts some UI probing event data from the given component. 
	 * @param src component to inspect.
	 * @return all extracted data in a map.
	 ************************************************************************/
	
	private Map<String, Object> extractData(Component src) {
		return dataExtractor.exctractData(src); 
	}

	/*************************************************************************
	 * Checks the given component with all registered filters to find out
	 * if we are interested in this kind of components. 
	 * @param src Component to inspect.
	 * @return true if it is accepted by all filters else false.
	 ************************************************************************/
	
	private boolean isAcceptedByComponentFilter(Component src) {
		for (UIMeterComponentFilter componentFilter : filterList) {
			if (componentFilter.accept(src) == false) {
				return false;
			}
		}
		return true;
	}

	/*************************************************************************
	 * Sets an data extractor that will extract all relevant probing event data
	 * from a component and concentrate it in a map structure.
	 * @param extractor The extractor to set.
	 ************************************************************************/
	
	public void setComponentDataExtractor(UIComponetDataExtractor extractor) {
		this.dataExtractor = extractor;
		if (this.dataExtractor == null) {
			this.dataExtractor = new DefaultDataExtractor();
		}
	}
	
	/*************************************************************************
	 * Adds an UIMeterComponentFilter to the probe.
	 * @param filter to reduce the list of probing event data to a set of selected
	 * components.
	 ************************************************************************/
	
	public void addComponentFilter(UIMeterComponentFilter filter) {
		this.filterList.add(filter);
	}

	/*************************************************************************
	 * Set the record execution time flag. If set the probe will produce
	 * additional UI action execution time data.
	 * @param active flag set it to true to gather execution time data.
	 ************************************************************************/
	
	public void setRecordExecutionTime(boolean active) {
		this.recordExecutionTime = active;
	}

	/*************************************************************************
	 * Checks if the probe gathers additional execution time data.
	 * @return Returns the current status of the recordExecutionTime flag.
	 ************************************************************************/
	
	public boolean isRecordExecutionTime() {
		return recordExecutionTime;
	}
	
/*****************************************************************************
 * Simple default implementation for the UIComponetDataExtractor
 * This data extractor will extract the following fields from the components:
 * - class name
 * - the "name" attribute of the component if set
 * - The text of all button implementations
 * 
 * From this data it should be possible to identify the clicked component.
 ****************************************************************************/

private static final class DefaultDataExtractor implements UIComponetDataExtractor {

	/*************************************************************************
	 * exctractData
	 * @see com.btr.uimeter.probes.UIComponetDataExtractor#exctractData(java.awt.Component)
	 ************************************************************************/
	
	public Map<String, Object> exctractData(Component src) {
		Map<String, Object> result = new HashMap<String, Object>();
		extractClassName(src, result);
		extractName(src, result);
		extractButtonText(src, result);
		return result;
	}

	/*************************************************************************
	 * @param src
	 * @param result
	 ************************************************************************/
	
	private void extractButtonText(Component src, Map<String, Object> result) {
		if (src instanceof AbstractButton) {
			String text = ((AbstractButton)src).getText();
			if (text != null && text.length() > 0) {
				result.put("text",  text);
			}
		}
	}

	/*************************************************************************
	 * @param src
	 * @param result
	 ************************************************************************/
	
	private void extractName(Component src, Map<String, Object> result) {
		if (src.getName() != null) {
			result.put("name",  src.getName());
		}
	}

	/*************************************************************************
	 * @param src
	 * @param result
	 ************************************************************************/
	
	private void extractClassName(Component src, Map<String, Object> result) {
		result.put("class",  src.getClass().getSimpleName());
	}
	
}


}